package net.shambolica.ember.node;

import com.ericsson.otp.erlang.OtpErlangAtom;
import com.ericsson.otp.erlang.OtpErlangException;
import com.ericsson.otp.erlang.OtpErlangObject;
import com.ericsson.otp.erlang.OtpErlangPid;
import com.ericsson.otp.erlang.OtpErlangTuple;
import com.ericsson.otp.erlang.OtpMbox;
import com.ericsson.otp.erlang.OtpNode;
import com.ericsson.otp.erlang.OtpErlangExit;
import com.ericsson.otp.erlang.OtpErlangDecodeException;
import net.shambolica.ember.utils.NodeUtils;
import static net.shambolica.ember.utils.StringUtils.isEmpty;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Random;

/**
 * An Ember Node is comprised of two actual nodes:
 * <br/>- Host node: a Jnode which contains the process linked to the Guest node (registered
 * as "ember_controller"), and the logger process (registered as "ember_logger").
 * <br/>- Guest node: a BEAM node which contains a process linked to the Host node (registered
 * as "ember_guest").
 * <br/>
 * <br/>The name of the host node is passed to the Guest node via environment variable "EMBER_HOST_NODE".
 * <br/>The name of the host controller process is passed to the Guest node via environment variable "EMBER_HOST_PROCESS".
 * <br/>The name of the host logger process is passed to the Guest node via environment variable "EMBER_HOST_LOGGER".
 * <br/>The name of the guest controller process is always "ember_guest".
 * <br/>The host controller process will be linked to the guest controller process. See method runGuest() for
 * handshake details. 
 */
public class EmberNode {

    protected Logger log = LoggerFactory.getLogger(EmberNode.class);

    public final static String REG_CONTROLLER = "ember_controller";
    public final static String REG_LOGGER = "ember_logger";
    public final static String REG_GUEST = "ember_guest";

    public final static String ENV_HOST_NAME_VAR = "EMBER_HOST_NODE";
    public final static String ENV_HOST_PROCESS_VAR = "EMBER_HOST_PROCESS";
    public final static String ENV_HOST_LOGGER_VAR = "EMBER_HOST_LOGGER";

    public final static String DEFAULT_BEAM = "erl";
    public final static long DEFAULT_START_TIMEOUT = 3000;
    
    protected final static SimpleDateFormat TS_FORMAT = new SimpleDateFormat("yyMMddHHmmss");


    private final String cookie;
    private final String hostNodeName;
    private final String guestNodeName;
    private final OtpNode hostNode;
    private final OtpMbox controllerMbox;
    private final OtpMbox loggerMbox;

    private volatile String beamExecutable;
    private volatile List<String> guestArguments;
    private volatile Process guestProcess;

    private volatile OtpErlangPid guestPid;

    private static volatile int serno;
    static {
        serno = new Random(System.currentTimeMillis()).nextInt(100000) + 1;
    }


    /**
     * On construction, we only create the local (Host) node and mailboxen.
     * @param cookieArg The cookie to use. Mandatory.
     */
    public EmberNode(String cookieArg) {
        if (isEmpty(cookieArg)) {
            throw new IllegalArgumentException("no cookie");
        }
        cookie = cookieArg;
        String idPostfix = createIdPostfix();
        hostNodeName = NodeUtils.createHostNodeName(idPostfix);
        guestNodeName = NodeUtils.createGuestNodeName(idPostfix);
        try {
            hostNode = new OtpNode(hostNodeName, cookie);
        } catch (IOException e) {
            log.error("unable to create host node '{}' - check epmd: {}", hostNodeName, e.getMessage());
            throw new RuntimeException("unable to create host node: "+e.getMessage(), e);
        }
        controllerMbox = hostNode.createMbox(REG_CONTROLLER);
        loggerMbox = hostNode.createMbox(REG_LOGGER);
        beamExecutable = DEFAULT_BEAM;
        guestArguments = new ArrayList<String>(0);
    }


    /**
     * Boot the guest.
     * Attempts to start the guest, then performs startup handshake:
     * <br/>Wait for {guest_started,EmberGuestPid} to be received at the controller mailbox.
     * <br/>Send {start_ack,EmberControllerPid} to EmberGuestPid.
     */
    public synchronized void runGuest(long timeout) {
        if (isGuestRunning()) {
            log.error("guest already running: {}", getGuestNodeName());
            throw new IllegalStateException("guest already running");
        }
        if (timeout <= 0) {
            timeout = DEFAULT_START_TIMEOUT;
        }

        List<String> cmd = new ArrayList<String>(getGuestArguments().size()+1);
        // Binary:
        cmd.add(getBeamExecutable());
        // Cookie and Node name:
        cmd.add("-setcookie");
        cmd.add(cookie);
        cmd.add("-sname");
        cmd.add(guestNodeName);
        // The rest of the args:
        cmd.addAll(getGuestArguments());

        ProcessBuilder pb = new ProcessBuilder(cmd);
        pb.environment().put(ENV_HOST_NAME_VAR, hostNodeName);
        pb.environment().put(ENV_HOST_PROCESS_VAR, REG_CONTROLLER);
        pb.environment().put(ENV_HOST_LOGGER_VAR, REG_LOGGER);

        try {
            guestProcess = pb.start();
        } catch (IOException e) {
            log.error("unable to execute '{}' :: {}", cmd, e.getMessage());
            throw new RuntimeException("unable to execute: "+e.getMessage(), e);
        }

        String error = null;
        OtpErlangObject hs1;
        try {
            hs1 = controllerMbox.receive(timeout);
        } catch (OtpErlangException e) {
            log.error("failed receiving handshake for '{}': {}: {}", new Object[]{
                    hostNodeName, e.getClass().getName(), e.getMessage()});
            log.info("killing guest process '{}'", guestNodeName);
            guestProcess.destroy();
            guestProcess = null;
            throw new RuntimeException("failed receiving handshake for "+hostNodeName, e);
        }

        if (hs1 == null) {
            error = "failed getting handshake";
        } else if (!(hs1 instanceof OtpErlangTuple)) {
            error = "handshake not a tuple";
        } else if (((OtpErlangTuple) (hs1)).arity() != 2) {
            error = "handshake wrong arity";
        } else if (!(((OtpErlangTuple) (hs1)).elementAt(0) instanceof OtpErlangAtom) ||
                !(((OtpErlangTuple) (hs1)).elementAt(0) instanceof OtpErlangPid)) {
            error = "handshake tuple bad data";
        } else if (!((OtpErlangAtom) ((OtpErlangTuple) (hs1)).elementAt(0)).atomValue().equals("guest_started")) {
            error = "bad handshake tag '" + ((OtpErlangAtom) ((OtpErlangTuple) (hs1)).elementAt(0)).atomValue();
        }
        if (error != null) {
            String message = "handshake error: " + error + " for '" + hostNodeName + "'";
            log.error(message, hostNodeName);
            guestProcess.destroy();
            guestProcess = null;
            throw new RuntimeException("handshake error: " + error + " for '" + hostNodeName + "'");
        } else {
            guestPid = (OtpErlangPid) ((OtpErlangTuple) (hs1)).elementAt(1);
            log.info("received handshake packet from {}, acknowledging ('{}')", guestPid, hostNodeName);
            controllerMbox.send(guestPid, new OtpErlangTuple(new OtpErlangObject[]{
                    new OtpErlangAtom("start_ack"), controllerMbox.self()}));

            try {
                controllerMbox.link(guestPid);
                log.info("guest is running ('{}')", guestNodeName);
            } catch (OtpErlangExit otpErlangExit) {
                log.info("guest has exited immediately after handshake, cleaning up");
                guestProcess.destroy();
                guestProcess = null;
                guestPid = null;
            }

        }
        
    }


    /**
     * Kill the guest OS process.
     * Expect to find
     */
    public synchronized void killGuest() {
        if (isGuestRunning()) {
            log.info("killing guest '{}'", guestNodeName);
            controllerMbox.unlink(guestPid);
            guestProcess.destroy();
            guestProcess = null;
            guestPid = null;
            log.info("guest '{}' is dead", guestNodeName);
        }
    }


    /**
     * Kill guest and host nodes.
     * Do not use this object past this point.
     */
    public synchronized void shutdown() {
        if (isGuestRunning()) {
            log.info("shutting down '{}'", hostNodeName);
            killGuest();
            hostNode.close();
        }
        controllerMbox.close();
        loggerMbox.close();
        hostNode.close();
        log.info("ember node '{}' is down", hostNodeName);
    }

    
    public synchronized void sendToGuestProcess(OtpErlangObject msg) {
        if (isGuestRunning() && msg != null) {
            controllerMbox.send(guestPid, msg);
        }
    }


    public OtpErlangObject readControllerMbox(long timeout) {
        return readMbox("controller", controllerMbox, timeout);
    }


    public OtpErlangObject readLoggerMbox(long timeout) {
        return readMbox("logger", loggerMbox, timeout);
    }


    private OtpErlangObject readMbox(String mboxName, OtpMbox mbox, long timeout) {
        if (isGuestRunning()) {
            try {
                if (timeout < 0) return mbox.receive();
                else             return mbox.receive(timeout);
            } catch (OtpErlangExit e) {
                synchronized (this) {
                    log.info("exit on {} '{}'", mboxName, hostNodeName);
                    killGuest();
                }
                return null;
            } catch (OtpErlangDecodeException e) {
                log.error("should never get here", e);
                return null;
            }

        } else {

            return null;

        }
    }


    /**
     * Create an id postfix.
     * @return A unique ID postfix.
     */
    protected static String createIdPostfix() {
        String ts = TS_FORMAT.format(new Date());
        int serno = getNextSerno();
        return ""+serno+"-"+ts;
    }


    /**
     * Provide a running serno, modulus 1000000 and never 0.
     * @return A serial number.
     */
    protected static int getNextSerno() {
        serno = (serno+1) % 1000000;
        if (serno == 0) {
            serno = 1;
        }
        return serno;
    }


    public synchronized boolean isGuestRunning() {
        return guestProcess != null;
    }


    public synchronized void setBeamExecutable(String beamExecutable) {
        if (isGuestRunning()) {
            log.error("guest already running: {}", getGuestNodeName());
            throw new IllegalStateException("guest already running");
        }
        if (isEmpty(beamExecutable)) {
            throw new IllegalArgumentException("null BEAM executable name");
        }
        this.beamExecutable = beamExecutable;
    }


    public synchronized void setGuestArguments(List<String> guestArguments) {
        if (isGuestRunning()) {
            log.error("guest already running: {}", getGuestNodeName());
            throw new IllegalStateException("guest already running");
        }
        if (guestArguments == null) {
            guestArguments = new ArrayList<String>(0);
        }
        this.guestArguments = guestArguments;
    }


    public Logger getLog() {
        return log;
    }

    public String getCookie() {
        return cookie;
    }

    public String getHostNodeName() {
        return hostNodeName;
    }

    public String getGuestNodeName() {
        return guestNodeName;
    }

    public OtpNode getHostNode() {
        return hostNode;
    }

    public OtpMbox getControllerMbox() {
        return controllerMbox;
    }

    public OtpMbox getLoggerMbox() {
        return loggerMbox;
    }

    public String getBeamExecutable() {
        return beamExecutable;
    }

    public List<String> getGuestArguments() {
        return guestArguments;
    }

    public Process getGuestProcess() {
        return guestProcess;
    }
}
